/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.camel.component.salesforce;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.salesforce.api.dto.AbstractQueryRecordsBase;
import org.apache.camel.component.salesforce.api.dto.UpsertSObjectResult;
import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatch;
import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatch.Method;
import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatchResponse;
import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatchResult;
import org.apache.camel.component.salesforce.api.utils.Version;
import org.apache.camel.component.salesforce.dto.generated.Account;
import org.apache.camel.test.junit5.params.Parameter;
import org.apache.camel.test.junit5.params.Parameterized;
import org.apache.camel.test.junit5.params.Parameters;
import org.apache.camel.test.junit5.params.Test;
import org.junit.jupiter.api.BeforeEach;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

@Parameterized
public class CompositeApiBatchManualIT extends AbstractSalesforceTestBase {

    public static class Accounts extends AbstractQueryRecordsBase<Account> {
    }

    private static final Set<String> VERSIONS
            = new HashSet<>(Arrays.asList(SalesforceEndpointConfig.DEFAULT_VERSION, "34.0"));

    private static final String ACCOUNT_EXTERNAL_ID = "CompositeAPIBatch";

    @Parameter
    protected String format;

    @Parameter(1)
    protected String version;

    private String accountId;

    @BeforeEach
    public void setupRecords() throws InterruptedException {
        if (accountId != null) {
            return;
        }

        final Account account = new Account();
        account.setName("Composite API Batch");
        account.setExternal_Id__c(ACCOUNT_EXTERNAL_ID);

        final UpsertSObjectResult result = template.requestBody(
                "salesforce:upsertSObject?sObjectIdName=External_Id__c", account, UpsertSObjectResult.class);
        accountId = result.getId();

        if (result.getCreated()) {
            // Give the indexer some time to index this account
            Thread.sleep(2000);
        }
    }

    @Test
    public void shouldSubmitBatchUsingCompositeApi() {
        final SObjectBatch batch = new SObjectBatch(version);

        final Account updates = new Account();
        updates.setName("NewName");
        batch.addUpdate("Account", accountId, updates);

        final Account newAccount = new Account();
        newAccount.setName("Account created from Composite batch API");
        batch.addCreate(newAccount);

        batch.addGet("Account", accountId, "Name", "BillingPostalCode");

        batch.addDelete("Account", accountId);

        final SObjectBatchResponse response = template.requestBody(batchUri(), batch, SObjectBatchResponse.class);

        assertNotNull(response, "Response should be provided");

        assertFalse(response.hasErrors());
    }

    @Test
    public void shouldSupportGenericBatchRequests() {
        final SObjectBatch batch = new SObjectBatch(version);

        batch.addGeneric(Method.GET, "/sobjects/Account/" + accountId);

        testBatch(batch);
    }

    /**
     * The XML format fails, as Salesforce API wrongly includes whitespaces inside tag names. E.g. <Ant Migration Tool>
     * https://www.w3.org/TR/2008/REC-xml-20081126/#NT-NameChar
     */
    @Test
    public void shouldSupportLimits() {
        final SObjectBatch batch = new SObjectBatch(version);

        batch.addLimits();

        final SObjectBatchResponse response = testBatch(batch);

        final List<SObjectBatchResult> results = response.getResults();
        final SObjectBatchResult batchResult = results.get(0);

        @SuppressWarnings("unchecked")
        final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();

        // JSON and XML structure are different, XML has `LimitsSnapshot` node,
        // JSON does not
        @SuppressWarnings("unchecked")
        final Map<String, Object> limits = (Map<String, Object>) result.getOrDefault("LimitsSnapshot", result);

        @SuppressWarnings("unchecked")
        final Map<String, String> apiRequests = (Map<String, String>) limits.get("DailyApiRequests");

        // for JSON value will be Integer, for XML (no type information) it will
        // be String
        // This number can be different per org, and future releases,
        // so let's just make sure it's greater than zero
        assertTrue(Integer.valueOf(String.valueOf(apiRequests.get("Max"))) > 0);
    }

    @Test
    public void shouldSupportObjectCreation() {
        final SObjectBatch batch = new SObjectBatch(version);

        final Account newAccount = new Account();
        newAccount.setName("Account created from Composite batch API");
        batch.addCreate(newAccount);

        final SObjectBatchResponse response = testBatch(batch);

        final List<SObjectBatchResult> results = response.getResults();

        final SObjectBatchResult batchResult = results.get(0);

        @SuppressWarnings("unchecked")
        final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();

        // JSON and XML structure are different, XML has `Result` node, JSON
        // does not
        @SuppressWarnings("unchecked")
        final Map<String, Object> creationOutcome = (Map<String, Object>) result.getOrDefault("Result", result);

        assertNotNull(creationOutcome.get("id"));
    }

    @Test
    public void shouldSupportObjectDeletion() {
        final SObjectBatch batch = new SObjectBatch(version);

        batch.addDelete("Account", accountId);

        testBatch(batch);
    }

    @Test
    public void shouldSupportObjectRetrieval() {
        final SObjectBatch batch = new SObjectBatch(version);

        batch.addGet("Account", accountId, "Name");

        final SObjectBatchResponse response = testBatch(batch);

        final List<SObjectBatchResult> results = response.getResults();
        final SObjectBatchResult batchResult = results.get(0);

        @SuppressWarnings("unchecked")
        final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();

        // JSON and XML structure are different, XML has `Account` node, JSON
        // does not
        @SuppressWarnings("unchecked")
        final Map<String, String> data = (Map<String, String>) result.getOrDefault("Account", result);

        assertEquals("Composite API Batch", data.get("Name"));
    }

    @Test
    public void shouldSupportObjectUpdates() {
        final SObjectBatch batch = new SObjectBatch(version);

        final Account updates = new Account();
        updates.setName("NewName");
        updates.setAccountNumber("AC12345");
        batch.addUpdate("Account", accountId, updates);

        testBatch(batch);
    }

    @Test
    public void shouldSupportQuery() {
        final SObjectBatch batch = new SObjectBatch(version);

        batch.addQuery("SELECT Id, Name FROM Account");

        final SObjectBatchResponse response = testBatch(batch);

        final List<SObjectBatchResult> results = response.getResults();
        final SObjectBatchResult batchResult = results.get(0);

        @SuppressWarnings("unchecked")
        final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();

        // JSON and XML structure are different, XML has `QueryResult` node,
        // JSON does not
        @SuppressWarnings("unchecked")
        final Map<String, String> data = (Map<String, String>) result.getOrDefault("QueryResult", result);

        assertNotNull(data.get("totalSize"));
    }

    @Test
    public void shouldSupportQueryAll() {
        final SObjectBatch batch = new SObjectBatch(version);

        batch.addQueryAll("SELECT Id, Name FROM Account");

        final SObjectBatchResponse response = testBatch(batch);

        final List<SObjectBatchResult> results = response.getResults();
        final SObjectBatchResult batchResult = results.get(0);

        @SuppressWarnings("unchecked")
        final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();

        // JSON and XML structure are different, XML has `QueryResult` node,
        // JSON does not
        @SuppressWarnings("unchecked")
        final Map<String, String> data = (Map<String, String>) result.getOrDefault("QueryResult", result);

        assertNotNull(data.get("totalSize"));
    }

    @Test
    public void shouldSupportRelatedObjectRetrieval() throws IOException {
        if (Version.create(version).compareTo(Version.create("36.0")) < 0) {
            return;
        }

        final SObjectBatch batch = new SObjectBatch("36.0");

        batch.addGetRelated("Account", accountId, "CreatedBy");

        final SObjectBatchResponse response = testBatch(batch);

        final List<SObjectBatchResult> results = response.getResults();
        final SObjectBatchResult batchResult = results.get(0);

        @SuppressWarnings("unchecked")
        final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();

        // JSON and XML structure are different, XML has `User` node, JSON does
        // not
        @SuppressWarnings("unchecked")
        final Map<String, String> data = (Map<String, String>) result.getOrDefault("User", result);

        final SalesforceLoginConfig loginConfig = LoginConfigHelper.getLoginConfig();

        assertEquals(loginConfig.getUserName(), data.get("Username"));
    }

    @Test
    public void shouldSupportSearch() {
        final SObjectBatch batch = new SObjectBatch(version);

        // we cannot rely on search returning the `Composite API Batch` account
        // as the search indexer runs
        // asynchronously to object creation, so that account might not be
        // indexed at this time, so we search for
        // `United` Account that should be created with developer instance
        batch.addSearch("FIND {Composite API Batch} IN Name Fields RETURNING Account (Name)");

        final SObjectBatchResponse response = testBatch(batch);

        final List<SObjectBatchResult> results = response.getResults();
        final SObjectBatchResult batchResult = results.get(0);

        final Object firstBatchResult = batchResult.getResult();

        final Object searchResult;
        if (firstBatchResult instanceof Map) {
            // the JSON and XML responses differ, XML has a root node which can
            // be either SearchResults or
            // SearchResultWithMetadata
            // furthermore version 37.0 search results are no longer array, but
            // dictionary of {
            // "searchRecords": [<array>] } and the XML output changed to
            // <SearchResultWithMetadata><searchRecords>, so
            // we have:
            // @formatter:off
            // | version | format | response syntax |
            // | 34 | JSON | {attributes={type=Account... |
            // | 34 | XML | {SearchResults={attributes={type=Account... |
            // | 37 | JSON | {searchRecords=[{attributes={type=Account... |
            // | 37 | XML |
            // {SearchResultWithMetadata={searchRecords={attributes={type=Account...
            // |
            // @formatter:on
            @SuppressWarnings("unchecked")
            final Map<String, Object> tmp = (Map<String, Object>) firstBatchResult;

            @SuppressWarnings("unchecked")
            final Map<String, Object> nested = (Map<String, Object>) tmp.getOrDefault("SearchResultWithMetadata", tmp);

            // JSON and XML structure are different, XML has `SearchResults`
            // node, JSON does not
            searchResult = nested.getOrDefault("searchRecords", nested.getOrDefault("SearchResults", nested));
        } else {
            searchResult = firstBatchResult;
        }

        final Map<String, Object> result;
        if (searchResult instanceof List) {
            @SuppressWarnings("unchecked")
            final Map<String, Object> tmp = (Map<String, Object>) ((List) searchResult).get(0);
            result = tmp;
        } else {
            @SuppressWarnings("unchecked")
            final Map<String, Object> tmp = (Map<String, Object>) searchResult;
            result = tmp;
        }

        assertNotNull(result.get("Name"));
    }

    @Override
    protected RouteBuilder doCreateRouteBuilder() throws Exception {
        return new RouteBuilder() {
            @Override
            public void configure() throws Exception {
                from("direct:deleteBatchAccounts")
                        .to("salesforce:query?sObjectClass=" + Accounts.class.getName()
                            + "&sObjectQuery=SELECT Id FROM Account WHERE Name = 'Account created from Composite batch API'")
                        .split(simple("${body.records}")).setHeader("sObjectId", simple("${body.id}"))
                        .to("salesforce:deleteSObject?sObjectName=Account").end();
            }
        };
    }

    SObjectBatchResponse testBatch(final SObjectBatch batch) {
        final SObjectBatchResponse response = template.requestBody(batchUri(), batch, SObjectBatchResponse.class);

        assertNotNull(response, "Response should be provided");

        assertFalse(response.hasErrors(), "Received errors in: " + response);

        return response;
    }

    @Parameters(name = "format = {0}, version = {1}")
    public static Iterable<Object[]> formats() {
        return VERSIONS.stream().map(v -> new Object[] { "JSON", v }).collect(Collectors.toList());
    }

    private String batchUri() {
        return "salesforce:composite-batch?format=" + format;
    }
}
